8W - 엔보이와 iptables 뜯어먹기

개요

이번 주차 스터디에서 마지막으로 다룬 내용은 이스티오 내에서 트래픽이 흘러가는 여정이다.
외부의 클라이언트가 게이트웨이로 요청을 보내 워크로드 파드로 전송되고, 파드 내에서 엔보이를 거쳐 어플리케이션에 도달해 돌아가는 과정.
이 내용은 쿠버네티스 클러스터에서 트래픽이 전달되는 과정까지 아우르는 굉장히 넓은 범위를 포괄한다.
나는 구체적으로 엔보이와 어플리케이션이 통신하고 트래픽이 움직이는 과정을 탐구하고자 한다.

세팅

클러스터에서 파드를 띄우고 이를 추적해보는 것은 상당히 번거롭다.
왜냐하면 컨테이너 기술은 네임스페이스를 분리시켜 호스트와 다른 네트워크인 것 같은 환경을 구성하기에 일차적인 격리가 발생하기 때문이다.
여기에 컨테이너 보안을 위해 컨테이너에서는 트래픽을 캡쳐하고 추적하는데 필요한 각종 관리자 권한을 막는 경우가 많아 트래픽을 추적하기 위해 들어가는 세팅이 상당하다.
그런데 이러한 컨테이너 환경에서의 불편함을 전부 깡그리 무시하고 이스티오의 트래픽을 캡쳐하는 방법이 있으니..
앞서 다룬 가상머신을 활용하는 것이다!

관련한 내용은 8W - 가상머신 통합하기에 담겨 있으니 참고!
이 레포의 코드를 그대로 베껴서 넣어도 된다.

사전 지식

사전 지식으로 iptables에 대한 이해가 요구된다.
해당 내용을 짧게 요약하기는 힘들 것 같아 이 문서로 옮기진 않겠다.

컨트랙

Connection Tracking, 줄여서 컨트랙은 넷필터 내부의 stateful 모듈이다.
이름 그대로 연결을 추적하는 역할을 하는데, 이것 덕분에 iptables는 stateful한 방화벽으로 기능할 수 있다.
iptables 이전에 ipchains라는 건 상태 추적이 없었다고 한다.[1]
컨트랙이 활성화돼있고 각 테이블들이 기본 정책으로 ACCEPT일 경우, 바운드되는 패킷은 nat 등의 룰의 적용을 받지 않는다.
그래서 iptables 룰을 설정하는 관점에서 응답 패킷에 대한 설정을 할 필요가 없어지므로 설정이 간소화되고 복잡성이 줄어든다.

자세한 정보는 따로 문서를 파서 정리할 것 같다.
참고하기 좋은 문서.[2]

프록시와 워크로드의 트래픽

서비스 메시의 아이디어는 프록시를 둬서 어플리케이션으로 드나드는 트래픽을 가로챈다는 것이고, 이스티오에서는 엔보이가 해당 프록시 역할을 수행한다.
여기에서 트래픽을 보내고 받는 대상을 규정하자면 외부, 엔보이, 어플리케이션이라 할 수 있다.
하지만 iptables를 보기 위해서는 패킷 관점에서 접근할 필요가 있다.

이걸로 끝인가?
아니, 모든 트래픽은 요청이 갔다가 돌아오는 응답이 있기 마련이므로, 위 4가지 분류 상의 트래픽은 각각 그에 상응하는 응답 트래픽도 존재한다.
이것을 왜 분류해야 하는가?
iptables은 엔보이와 어플리케이션이 돌아가는 프로세스 밑단에서 네트워크를 필터링하고 처리하기 때문이다.
잠시 언급한 컨트랙을 통해 대부분의 응답 트래픽이 잘 처리되긴 하나, 이스티오에서는 DNS 관련하여 컨트랙 관련 추가 룰을 작성한다.
이 부분에 대해서 아래에서 더 자세히 보겠다.
아무튼 요청 이후에는 항상 응답 패킷도 있다는 것은 염두해두자.

system-resolved

추가적으로, 실습을 진행한 환경은 우분투로, 우분투는 기본적으로 system-resolved라는 데몬이 활성화돼있다.
이 데몬은 외부 네임 서버에 질의를 해주는 대리자로, 로컬에서의 모든 DNS질의는 일단 이 데몬을 거친다.
로컬의 질의는 /etc/resolve.conf에 지정된 주소로 날아가는데 여기에 127.0.0.53:53으로 system-resolved의 주소가 적혀있다.
이것 때문에 패킷 추적에 조금 어려움이 있긴 한데, 이렇게 리졸버 데몬이 띄워둔 환경에서도 DNS 프록시 기능은 잘 작동한다.

본격 iptables 분석


지미 송이 도식화한 파드 내에서 전체 트래픽이 움직이는 과정이다.[3]
iptables로 조작되는 트래픽의 흐름이 상세하게 적혀있다.
전체 도식을 한번에 표현해서 되려 복잡하게 느껴지기도 한다..
이것만으로 모든 규칙과 흐름이 이해된다면, 이 글은 읽을 필요 없다.

적용된 iptables의 상태를 간단하게 덤프 떠서 보자.

iptables-save

image.png
요런 식으로 확인할 수 있다.
mangle 테이블에 dns 관련 체인이 들어가는 것도 확인할 수 있다.

테이블 형태로 확인하는 것이 그래도 조금 더 보기는 편하지 않은가 싶다.

iptables -t nat -L -n -v
iptables -t raw -L -n -v

단순하게 테이블 별로 체인 리스트를 보는 명령어이다.
n 옵션으로 웰논 포트와 주소가 대체되지 않도록 하는 것이 좋은데, 이름으로 대체되면 127.0.0.6이나 127.0.0.1을 같은 이름으로 출력하는 등 제대로 확인이 힘들어진다.
또한 완전한 규칙을 확인하려면 여기에 꼭 v 인자를 넣어야 한다!
그래야만 드나드는 인터페이스 기준도 보이기 때문이다.
(이거 빼고 봐도 규칙 자체는 풀로 보이는 줄 알고 헤맸다..)

이제부터 이걸 본격적으로 탐구해본다.
패킷 경로를 정하는, 가장 기본이 되는 nat 테이블은 이런 모양을 가지고 있다.

Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
  281 15320 ISTIO_INBOUND  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
 6341  420K ISTIO_OUTPUT  all  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain ISTIO_INBOUND (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:15008
    2   148 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
    0     0 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:15090
    0     0 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:15021
    0     0 RETURN     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:15020
  279 15172 ISTIO_IN_REDIRECT  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain ISTIO_IN_REDIRECT (3 references)
 pkts bytes target     prot opt in     out     source               destination
  280 15232 REDIRECT   tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            redir ports 15006

Chain ISTIO_OUTPUT (1 references)
 pkts bytes target     prot opt in     out     source               destination
 4261  256K RETURN     all  --  *      lo      127.0.0.6            0.0.0.0/0
    1    60 ISTIO_IN_REDIRECT  tcp  --  *      lo      0.0.0.0/0           !127.0.0.1            multiport dports  !53,15008 owner UID match 998
    9   540 RETURN     tcp  --  *      lo      0.0.0.0/0            0.0.0.0/0            tcp dpt:!53 ! owner UID match 998
  146 10460 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            owner UID match 998
    0     0 ISTIO_IN_REDIRECT  tcp  --  *      lo      0.0.0.0/0           !127.0.0.1            tcp dpt:!15008 owner GID match 998
    0     0 RETURN     tcp  --  *      lo      0.0.0.0/0            0.0.0.0/0            tcp dpt:!53 ! owner GID match 998
    0     0 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            owner GID match 998
 1924  153K ISTIO_OUTPUT_DNS  all  --  *      *       0.0.0.0/0            0.0.0.0/0
   81  9698 RETURN     all  --  *      *       0.0.0.0/0            127.0.0.1
 1768  137K ISTIO_REDIRECT  all  --  *      *       0.0.0.0/0            0.0.0.0/0

뭔가 여러 룰들이 들어있는 것이 보이는데, 얼추 보면 인바운드와 아웃바운드에 대한 규칙들이 작성됐음을 확인할 수 있다.
raw 테이블은 어떻게 생겼는가?

Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         
ISTIO_INBOUND  all  --  0.0.0.0/0            0.0.0.0/0           

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         
ISTIO_OUTPUT_DNS  all  --  0.0.0.0/0            0.0.0.0/0           

Chain ISTIO_INBOUND (1 references)
target     prot opt source               destination         
CT         udp  --  127.0.0.53           0.0.0.0/0            udp spt:53 CT zone 1

Chain ISTIO_OUTPUT_DNS (1 references)
target     prot opt source               destination         
CT         udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:53 owner UID match 998 CT zone 1
CT         udp  --  0.0.0.0/0            0.0.0.0/0            udp spt:15053 owner UID match 998 CT zone 2
CT         udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:53 owner GID match 998 CT zone 1
CT         udp  --  0.0.0.0/0            0.0.0.0/0            udp spt:15053 owner GID match 998 CT zone 2
CT         udp  --  0.0.0.0/0            127.0.0.53           udp dpt:53 CT zone 2

raw 테이블에서는 dns 관련 컨트랙을 설정하는 룰이 들어있다.
각 룰의 맨 끝에 CT zone 1, 2라고 설정하는 것이 컨트랙의 지역을 설정하는 것으로, 임의의 어떤 트래픽들을 같은 트래픽으로 추적되지 않고 두 가지로 분리하여 인식할 수 있도록 추가적인 설정을 하는 것이다.

역시 그냥 보면 좀 어렵다고 생각해서, 각 상황 별로 발생하는 트래픽이 어떤 흐름을 타게 되는지 단계 별로 구조화해본다.
참고로 현 인스턴스에서 istio-proxy는 998 UID, GID를 가지고 있는데, 클러스터에 배치되는 프록시를 보면 1337을 UID, GID로 고정적으로 가진다.

룰에 걸리는 패킷의 변화를 유동적으로 추적하고 싶다면 이렇게 watch를 걸어두는 것을 추천한다.

CHAIN="PREROUTING ISTIO_INBOUND ISTIO_IN_REDIRECT ISTIO_OUTPUT ISTIO_OUTPUT_DNS ISTIO_REDIRECT"
watch -d "for i in $CHAIN ;do iptables -t nat -n -v -L \$i; echo; done"

CHAIN 문 안에 원하는 체인의 이름을 나열해주면 된다.
INPUT 같은 체인은 볼 필요가 없어서 빼기 위해서 이렇게 사용했다.

색깔 표기

조금이라도 간소화시켜 작성하기 위해 도식에 색을 사용했다.
리턴 타겟을 가진 룰은 빨간색으로 해당 체인을 빠져나가 이전 체인으로 돌아가 룰 적용을 받는다.
파란색으로 작성한 룰은 별도의 조건이 명시되지 않아 해당 룰에 걸리는 패킷은 모두 적용을 받게 된다.

nat 테이블 - PREROUTING 체인

먼저 들어오는 트래픽이 어떻게 처리되는지 확인해보자.

PREROUTING 체인에 걸리는 패킷은 다음의 순서를 거친다.
사실 이쪽은 그렇게 어렵지 않다.
간단하게 말해 위의 몇 가지 포트를 목적지로 하는 패킷들만 그냥 진행되고, 나머지 모든 패킷들은 엔보이의 인바운드 핸들러 포트로 전달된다.

HBONE?

HBONE은 HTTP-Based Overlay Network Environment라 하여, 이스티오 컴포넌트 간 보안 터널링 프로토콜을 말한다.
여러 TCP 스트림을 하나의 mtls 커넥션으로 처리하기 위해 존재하며, 해당 통신 포트가 15008이다.
앰비언트 모드에서만 쓰이는 건지는 이후 앰비언트 모드까지 공부해야 명확해지겠다.[4]

이후에 INPUT 체인도 거치게 될 텐데, 해당 부분에는 아무런 체인이 걸려있지 않으므로 그대로 통과되기에 기재하지 않았다.

nat 테이블 - OUTPUT 체인

사실 나가는 트래픽 처리가 꽤 복잡하다.
위에서 말했듯이 나가는 트래픽을 세분화시켜야 하기 때문이다.

처음 봤을 때는 이게 뭐지 싶었는데, 7번에 있는 가장 마지막 룰에 적용을 받기 이전에 걸러낼 트래픽들을 걸러내는 과정이라고 생각하면 한결 편하다.
7번은 15001포트, 즉 엔보이가 나가는 트래픽을 처리하는 아웃바운드 핸들러로 패킷을 보내는 규칙이다.
그러니 엔보이가 보내는 패킷이 7번에 도달하면 다시 패킷이 엔보이로 들어가는 불상사가 발생할 것이다!
여러 상세한 규칙이 설정된 게 바로 이러한 이유 때문이다..

각 번호의 룰이 담당하는 의미를 분석해보자.

분석을 해봐도, 사실 엄청 명쾌하진 않다.
2번 규칙이 사실 이 모든 걸 어렵게 만드는 포인트라고 생각하는데, 아무튼 발생하는 트래픽 케이스 별로 아래에서 흐름도를 다시 정리하겠다.
여기에서는 이런 룰들이 있고, 이 룰들이 이런 것에 적용되나보다 정도만 머리에 담고 넘어간다.

raw 테이블

마지막으로 DNS의 컨트랙을 관리하는 raw 테이블을 보도록 한다.

raw 테이블에서는 컨트랙을 논리적으로 구분 짓기 위해 zone 설정을 한다. 컨트랙이 제대로 연결되면 요청 패킷에 대한 응답 패킷이 nat 테이블의 적용을 받지 않기 때문에 룰 설정이 상대적으로 간편해진다. 컨트랙은 `프로토콜:출발지 주소:출발치 포트:목적지 주소: 목적지 포트`로 대칭되는 패킷을 결정짓는데, 이스티오가 아무리 장난질을 쳐도 애플리케이션과 통신할 때는 127.0.0.6 주소를 쓴다던가 하는 식으로 차별을 나름 두기 때문에 컨트랙에 문제가 발생하지 않는다.

문제가 생길 여지가 있는 건 바로 DNS 질의를 할 때이다.

뇌피셜

혼자 머리를 싸매며 패킷 경로를 추적해서 원인을 추론해낸 거라 정확하지 않을 수 있다.
아직 넷필터 모듈의 전반적인 동작을 잘 아는 게 아니라 오히려 틀릴 가능성이 있다는 것도 염두해둔다.

DNS 프록시로서 기능하는 pilot-agent는 iptables 상에서 DNAT되어 어플리케이션의 DNS 질의를 받게 된다. 그리고 자신도 모르면 결국 어플리케이션이 보내려던 경로로 똑같이 질의를 날리게 된다. 그런데 컨트랙을 하기 위한 패킷 추적은 nat 테이블보다 먼저 적용되는 raw 테이블의 OUTPUT 체인이다. 결국 질의를 날리는 두 패킷 모두 목적지가 127.0.0.53:53으로 같기 때문에 혼동이 발생할 수 있다. 이러한 문제를 미연에 방지하고 정확하게 컨트랙을 할 수 있도록 raw 테이블에서 zone을 분리한다. (어플리케이션의 소스 포트와 에이전트의 소스 포트가 무조건 달라서 상관 없을 것 같기도..)

아무튼 도식 상에서는 zone을 딱 2개로 나눈다.
그래도 DNS의 작업은 결국 크게 두 가지로 나뉘기 때문에 엄청 어렵진 않다.

결론적으로 어플리케이션이 에이전트에 보내는 DNS 질의는 zone 1, 에이전트가 리졸버에 보내는 질의는 zone 2로 기록되는 것이다.

상황 별 패킷 흐름과 엔보이 설정 정보

그럼 실제 트래픽이 지나가는 경로를 명확하게 밝혀보자.
패킷의 흐름을 조금 더 시각적으로 확인하기 위해 와이어샤크를 활용했다.

# vm에서
tcpdump -i any -c 1000 -w /home/ubuntu/capture1.pcap

dig +short naver.com

# 로컬에서
scp -i infra-terraform/key.pem -r ubuntu@$FORUM:/root/capture.pcap .

처음에는 termshark를 이용해 터미널 환경에서 분석을 했지만, 시각화가 자유롭지는 않은 것 같다고 느껴서 덤프 파일을 가져오는 식으로 진행했다.

DNS 질의 트래픽

클러스터 도메인으로 보내는 트래픽은 에이전트에서 모든 응답을 마치기 때문에 그다지 확인할 지점이 없다고 판단하여 외부 도메인 질의를 해봤다.
image.png
참고로 시스템 리졸버라는 놈도 사실은 로컬 환경의 dns 질의를 앞단에서 처리해주는 데몬이라 거의 프록시 느낌이다.
현 인스턴스에서 실제 답변을 진행하는 네임서버는 192.168.0.2에 있다.

dig +short github.com

먼저 패킷 캡쳐 그림이다.
image.png
총 세 번의 질의가 있었고, 스택에서 원소를 꺼내듯이 응답이 돌아갔다.

그리고 거꾸로 응답이 돌아왔다.
이 흐름 자체는 위의 과정을 생각해보면 얼추 당연할 것이다.

그렇다면 iptables 상에서는 어떻게 요청 패킷이 가고 응답 패킷이 돌아올까?
다른 것들도 일일히 도식을 그려본 바로는, 사실 DNS 질의 추적이 가장 어렵다..
먼저 질의 한번에 영향을 받은 룰을 살펴보자면,
image.png
이렇게 여러 룰이 한꺼번에 카운트되는 것을 확인할 수 있다!
처음에 이걸 어떻게 해석해야 하나 많이 난감했는데, 구체적으로 따져보니 이 정도로 도식을 그릴 수 있겠다.

(OUTPUT 체인은 너무 당연해서 표기하지 않았다.)
ISTIO_OUTPUT 체인을 보면 4가지 룰에 패킷 카운트가 되는 것을 확인할 수 있는데, 이들을 보라색으로 표기했다.
지금까지 이해한 패킷의 흐름은 다음과 같다.

컨트랙의 동작을 잘 몰라서 처음에 이해하는 게 너무 어려웠다.
사실 지금도 명확하게 안다고 이야기는 못 하겠지만, 이렇게 해석했을 때 패킷 카운트의 변화가 잘 해석돼서, 이게 맞다고 생각한다.

conntrack -L -p udp
bpftrace -e 'tracepoint:syscalls:sys_enter_connect { printf("%d %s\n", pid, comm); }'

정확하게 추적해보려고 여러 시도를 해봤다..
시스템 리졸버가 있어서 트래픽이 엄청 꼬였지, 사실 리졸버 없이 바로 외부 네임서버로 질의가 이뤄졌다면 패킷 흐름은 훨씬 단순화됐을 것으로 보인다.

인바운드 트래픽

패킷 수집은 계속 같은 방법을 활용하고, 대신 어떤 위치에서 요청을 하냐에 따라서만 차이를 뒀다.

# vm에서
tcpdump -i any -c 100 -w /home/ubuntu/capture2.pcap
# 로컬에서
scp -i infra-terraform/key.pem -r ubuntu@$FORUM:/home/ubuntu/capture2.pcap .

image.png
외부에서 먼저 들어오는 요청은 iptables의 영향을 받는 과정을 추적하는 게 조금 까다롭다.
왜냐하면 tcpdump는 인터페이스를 지나는 시점의 트래픽을 뜯어내기 때문이다.
외부에서 들어오는 트래픽은 PREROUTING 테이블에 진입하기 이전에 수집된다.
반면 내부에서 나가는 트래픽은 인터페이스로 가기 전에 OUTPUT테이블을 거치고 수집된다.
그래서 이 둘 간의 차이를 인식하고 패킷을 들여다봐야 한다.

image.png
먼저 패킷 카운트는 이렇게 일어난다.
인바운드 트래픽은 확인이 편한 편이다.

응답 패킷은 컨트랙돼서 iptables에서 추적되지 않는다.
외부의 요청은 특정 포트가 아닐 때는 무조건 ISTIO_IN_REDIRECT로 빠지는데 이때 envoy의 인바운드 핸들러 포트 15006으로 빠진다.
그리고 엔보이는 127.0.0.6의 주소를 가지고 어플리케이션에 요청을 하기에 PREROUTING 체인을 거치지 않고 전달된다.
127.0.0.6은 ISTIO_OUTPUT 체인의 첫번째 룰로 바로 리턴되며, 결과적으로 어플리케이션에 바로 전달될 것이다.

여기에서 엔보이 설정도 잠시 확인해본다.

curl localhost:15000/config_dump | istioctl pc listener -f -

istioctl을 vm에 설치하면 설정을 그나마? 편하게 볼 수 있다.
image.png
보다시피 0.0.0.0:15006은 목적지가 InboundPassthroughCluster라고 표시된다.
image.png
해당 포트의 설정을 조금 더 자세히 보자면, 일단 해당 포트에 대한 리스너 이름은 virtualInbound이다.
그리고 리스너의 필터체인에 리스트로 여러 체인이 걸려 있는데, 각각은 filterChainMatch로 매칭된 트래픽을 처리할 방침을 정해두고 있다.
맨 위, 인바운드 포트로 직빵으로 날리는 요청에 대해서는 virtualInbound-blackhole, 즉 블랙홀로 빠뜨려버린다!
image.png
이후 체인들은 대체로 비슷하게 생겼다.
근데 아무튼 InboundPassthroughCluster 라우트 설정을 적용하며, 이때 가상호스트 이름을 inbound|http|0으로 적용한다.
InboundPassthroughCluster라는 클러스터로 요청이 갈 것이란 것도 확인할 수 있다.
그럼 라우트 설정도 확인해보자.

curl localhost:15000/config_dump | istioctl pc -f - route

image.png
중간에 위에서 봤던 InboundPassthroughCluster가 확인된다!
가상호스트 이름도 일치한다.
image.png
여기 들어있는 설정은 위에서 본 리스너 설정에 일부만 표시된다.
route라는 게 리스너 필터 체인 중 HCM에 대한 설정이니 사실 당연한 것 같다.

다음은 클러스터에 대해 살펴본다.
엔보이의 작동 구조를 안다면, 결국 자신이 프록싱하는 어플리케이션 역시 클러스터 중 하나라는 것을 사실 쉽게 알 수 있다.

curl localhost:15000/config_dump | istioctl pc -f - cluster --fqdn InboundPassthroughCluster

image.png
127.0.0.6이란 주소가 드디어 등장했다!
클러스터 중 ORIGINAL_DST 타입은 들어온 목적지 주소 그대로 보낸다는 의미를 가지고 있다.[5]
여기에 출발지 주소를 127.0.0.6으로, 즉 엔보이 자기 자신의 주소를 저렇게 매긴다.
그래서 어플리케이션에서 소스 ip를 까보면 죄다 127.0.0.6이 나오는 것이다.

번외 - 엔보이의 업스트림 커넥션

처음에 명확하게 인식하지 못해 헤맨 포인트가 있었는데, 서비스에서 가상머신으로 반복접근할 때 PREROUTING이 증가하지 않는 이슈가 있었다.

curl -is $FORUM:8080/api/users | grep HTTP 
k -n istioinaction exec -ti catalog-5899bbc954-xpqqg -- curl forum.forum-services/api/users

내 로컬에서 그냥 요청을 보낼 때는 문제없이 지속적으로 PREROUTING으로 패킷이 드나드는 것이 확인됐다.
그러나 서비스에서 실행할 때는 PREROUTING의 패킷이 처음 요청을 보낼 때만 카운트되고 이후에는 변화 없이 트래픽이 반환됐다.
물론 실제 요청은 가상머신에서 실행되는 것이 확실했다.
conntrack을 지우고 다시 했을 때 제대로 카운트가 되는 것을 확인했다.
찾아보니 컨트랙 정보가 저장될 때, nat와 같은 iptables 단에서 설정되는 정보들을 저장하기 때문에 같은 정보가 들어오면 컨트랙 모듈이 nat 테이블을 우회할 수 있도록 해준다는 모양이다.
image.png
패킷을 뜯었을 때 조금 더 명확하게 할 수 있었는데, 일단 엔보이 기본 설정이 KeepAlive를 통해 TCP 커넥션을 유지한다.
덕분에 클러스터에서 가상머신으로, 심지어 가상머신에서 외부로 요청을 보낼 때도 지속적으로 같은 포트를 이용해서 통신한다..
192.168.10.200:55320은 vm의 포럼 서비스에서 외부의 jsonplaceholder 사이트로 보내는 요청이다.
근데 이게 내가 요청을 3번을 보내는 동안 계속 반복돼서 사용됐다.
내 로컬에서 요청을 보낼 때는 내 로컬이 keep alive 헤더를 포함시키지 않고 커넥션 풀을 유지하지도 않기 때문에 매번 새로운 커넥션으로 인식이 된 것이다.

아웃바운드 트래픽

그럼 나가는 트래픽은 어떠한가?
image.png
동서 게이트웨이에 통신할 때는 mtls가 적용되기 때문에, 커다란 크기를 가진 TLS 통신 패킷이 보인다.
image.png
참고로 ISTIO_OUTPUT_DNS 체인은 무조건 들어가게 돼있다.
안 속 룰에 조건이 걸려 있어 DNS 질의가 아니면 그냥 나온다.
그리고 동서 게이트웨이에 이미 컨트랙이 걸려 있어서 그런지 테이블에서 패킷이 추적되지 않은 것으로 보인다.
어차피 엔보이로 들어가는 트래픽의 흐름이 핵심이기 때문에 해당 패킷이 카운트되지 않는 것은 신경 쓰지 않았다.

여태 봤던 패킷 흐름 중에선 그래도 가장 간단한 것 같기도..?
아무튼 ISTIO_OUTPUT 체인의 여러 룰에 하나도 걸리지 않은 패킷은 마지막 룰인 ISTIO_REDIRECT 체인으로 가게 된다.
그리고 해당 체인은 15001 포트로 패킷을 리다이렉트시키기에 결과적으로 엔보이로 트래픽이 흘러가는 것을 확인할 수 있다.
15001은 엔보이의 아웃바운드 핸들러 포트로, 해당 포트로 들어온 패킷을 기반으로 외부 통신을 진행한다.

그럼 정말 15001에선 무슨 일이 일어나는가?

curl localhost:15000/config_dump | istioctl pc -f - listener

image.png
보다시피 엔보이의 리스너 설정에서는 PassthroughCluster를 목적지로 삼고 있는 것을 볼 수 있다.
image.png
조금 더 자세히 보면, 트래픽 방향은 OUTBOUND로 나오며 라우트 설정이 없는 것을 확인할 수 있다.
그저 패스스루클러스터로 보내기만 하는 것으로 보이나, 실상 여기에서 다른 리스너 매칭이 이뤄진다.[6]
이스티오 문서에서는 자세히 언급이 없어서 구체적으로 어떤 설정 때문에 이게 가능한지 찾아봤다.[7]
image.png
15001 포트를 리스닝하는 이 virturlOutbound는 useOriginalDst값이 참이다.
이로부터 받을 때는 15006 포트 소켓으로 패킷을 받았지만 원래 목적지 주소에 해당하는 리스너에게 커넥션을 그냥 넘겨버린다.
해당하는 조건에 매칭되는 리스너가 없다면 비로서 위 패스스루 클러스터로 빠지는 구조이다.
패스스루 클러스터로 넘어가면 이때는 그냥 주소 기반으로 트래픽을 날려버린다.
이렇게 될 경우 라우트 설정에 들어간 세밀한 트래픽 제어 설정은 기대할 수 없게 될 것이다.

자기 자신 호출 트래픽

마지막으로 볼 것은 처음 ISTIO_OUTPUT 테이블을 봤을 때 가장 이해를 어렵게 만드는 2번 룰에 대한 패킷이다.
2번 룰은 자기 자신을 호출할 때 발동된다.
다시 2번의 조건을 상기시키자면,

  1. istio-proxy가 보낸 패킷이어야 한다.
  2. 127.0.0.1이 아니어야 한다.
  3. lo 인터페이스를 타야 한다.
  4. 목적지 포트가 53, 15008가 아니어야 한다.

2번과 3번 조건은 꽤나 쉬워보는 게 127.0.0.0/8 내부는 전부 lo 인터페이스를 타니 저 범위에 해당하는 임의의 주소로 요청을 보내면 될 것 같지만..
그러한 요청을 어떻게 해야 istio-proxy가 날리는가가 관건이다.
그냥 curl 127.0.2.0 이런 식으로 요청을 날리면, 해당 요청은 15001을 타고 엔보이가 요청을 수행하기 이전에 3번 룰에 의해 리턴되어버린다.
istio-proxy가 아니면서 바로 lo 인터페이스를 타버리기 때문에 엔보이가 관리하지 않게 된다는 것.

그렇다면 자기 자신을 가리키는 다른 인터페이스의 주소라면 어떨까?
curl 192.168.10.200으로 요청을 보내보면 여전히 원하는 매칭은 이뤄지지 않는 것을 확인할 수 있다.
image.png
이유는 사실 간단하다.
라우팅 테이블 룰 상에서는 local이 가장 먼저 적용되는데, 여기에 각 인터페이스 별 자신이 가진 주소가 적혀있다.
이 테이블에 걸리면 lo 인터페이스를 타게 되므로 해당 요청도 정답이 아니다.

이게 가능한 케이스는 서비스 메시 내에서 부여된 자기 자신의 호스트나 주소로 요청을 보내는 것이다.
왜 이렇게 되는지 이제부터 보도록 한다.
image.png
vm에는 forum이 가동되고 있어 curl forum과 같은 식으로 요청을 날렸다.
15001을 탔다가 15006을 탔다가 난리 부르스를 피는 게 보인다!
image.png
드디어 2번 룰에 의해 패킷이 ISTIO_IN_REDIRECT 체인에 들어가게 됐다.
해당 체인에 들어가면 15006 포트로 들어가게 되므로 위와 같은 결과가 나온 것이다.
dns 관련 패킷을 제외하고, 인바운드를 처리하는 ISTIO_IN_REDIRECT체인과 아웃바운드를 처리하는 ISTIO_REDIRECT 체인이 카운트되는 것을 확인할 수 있다.

이 케이스는 홉이 많아서 직관적으로는 이냥저냥이어도 막상 패킷을 뜯어보면 또 복잡하다.
순서대로 정리해보자면,

응답은 전부 역순이다.
패킷 덤프 상에서 중간에 404 응답이 없어서 헷갈렸는데, 이건 중간에 mtls 통신이 이뤄져서 그렇다.
구체적으로는 엔보이가 혼자 패킷을 주고받는 과정에서 mtls가 이뤄진다.
해당 내용물을 까볼 순 없어도 PSH(push 제어 플래그. 데이터를 바로 넘겨라)를 통해 통신이 되고 있단 것은 확인할 수 있다.

결과적으로, 자기 자신을 호출하되 서비스 메시 내부의 트래픽으로서 자신을 호출한다면 이는 결국 메시의 트래픽 제어를 받게 된다.
인바운드 핸들러를 거쳐서 자신에게 돌아오기 때문이다!

결론

원래 생각은 iptables 조금 해석해보고 말 생각이었는데, 도무지 이해가 안 되는 룰이 있어서 결국 까보게 됐다.
iptables에서 테이블 간 적용 순서가 항상 헷갈렸는데, 이번에 이렇게 삽질을 해대니 이제 그 흐름이 살짝 체화됐다고 해야 하나, 자연스럽게 느껴진다.
iptables의 룰이 상당히 복잡하게 짜여진 것으로 보이지만, 이러한 규칙을 적용했기에 다양한 상황의 패킷을 처리할 수 있게 된다.
사실 자기 자신을 호출하는 트래픽을 제어하지 않을 요량이었다면 룰은 훨씬 간소화됐을 것이다.
그럼에도 메시 내 트래픽 제어라는 서비스 메시의 기능을 엄격하게 지켜내는 방향으로 철저하게 설계했고, 또 그렇게 구현됐기에 이스티오가 또 이렇게 유명해질 수 있었던 게 아닐까 생각한다.

iptables 공부가 꽤 많이 된 것 같다..
image.png
uid나 gid로 매칭이 가능한 이유가 이렇다고 한다.

번외 - deb 파일 분석

현재 환경이 우분투 환경이라 데비안 패키지 파일을 이용했다.
이전에 결과적으로 확인해보기는 했으나 이 파일은 구체적으로 무슨 동작을 할까?
간단하게 내부 파일을 추적해보자.

deb은 ar 형식의 파일로, 먼저 파일의 압축을 해제한다.

ar x istio-sidecar.deb

image.png
내부에는 세부 압축 파일이 들어있다.
이 파일들은 deb 패키지 설치 시에 어떻게 동작하는가?
image.png
control 파일은 실제 행동하는 동작에 대한 스크립트와 메타데이터 값이 들어있다.
그리고 data 파일에는 패키지를 설치할 때 파일시스템에 들어가게 될 각종 파일들을 담고 있다.

각 tar파일을 뜯어보면 구체적으로 이들이 무엇을 하는지 알 수 있다.

tar -xf control.tar.gz

image.png
결과물은 이렇게 생겼다.
위에서 봤듯이 postinst가 data파일의 내용물이 파일시스템으로 옮겨진 후 실제 동작을 수행할 것을 추측할 수 있다.

tar -xf data.tar.gz

image.png
data 파일 안에는 var, lib, usr 세 가지 디렉토리가 들어있다.
image.png
data 파일에 실제로 사용되는 각종 파일들이 들어있고, 이들은 패키지 설치 시 각 위치에 덮어씌워질 것이다.

그럼 postinst에서는 구체적으로 무슨 동작을 하는가?
image.png
해당 파일은 쉘 스크립트로 짜여져 있기 때문에 쉽게 내용물을 확인해볼 수 있다.
보다시피 istio-proxy라는 유저와 그룹을 만들고, data 파일들에 istio-proxy로 소유권을 이관하는 작업을 수행한다.
(참고로 mkdir -p는 해당 디렉토리가 없을 경우 생성한다.)
image.png
여기에 마지막으로 추가적인 작업이 들어가는데, envoy와 pilot-agent 파일이 실행될 때의 소속을 수정하는 작업이다.
위에서 두 파일은 이미 istio-proxy로 유저와 그룹이 세팅됐다.
그러나 iptables 조작을 위해 해당 파일들은 루트 권한으로 실행돼야 한다.
그래서 어떤 유저로 실행되더라도 그룹 id는 istio-proxy로 걸어두기 위해 chmod의 맨 앞자리를 2로 설정하는 것이다!

참고로 chmod에서 맨 앞자리 특수 권한 비트는 다음의 의미를 가진다.

아무튼 deb 패키지를 설치했을 때 설정되는 것들을 요약하자면..

이 정도가 있겠다.
처음에 세팅되는 것을 보고 혹시 istio-proxy라는 유저와 그룹이 무조건 998로 고정되는 건가 궁금해서 뜯어봤는데, 사실은 그냥 이전에 만들어진 유저가 다른 게 없어서 가장 먼저 998을 받은 거였다.

이전 글, 다음 글

다른 글 보기

이름 index noteType created
1W - 서비스 메시와 이스티오 1 published 2025-04-10
1W - 간단한 장애 상황 구현 후 대응 실습 2 published 2025-04-10
1W - Gateway API를 활용한 설정 3 published 2025-04-10
1W - 네이티브 사이드카 컨테이너 이용 4 published 2025-04-10
2W - 엔보이 5 published 2025-04-19
2W - 인그레스 게이트웨이 실습 6 published 2025-04-17
3W - 버츄얼 서비스를 활용한 기본 트래픽 관리 7 published 2025-04-22
3W - 트래픽 가중치 - flagger와 argo rollout을 이용한 점진적 배포 8 published 2025-04-22
3W - 트래픽 미러링 패킷 캡쳐 9 published 2025-04-22
3W - 서비스 엔트리와 이그레스 게이트웨이 10 published 2025-04-22
3W - 데스티네이션 룰을 활용한 네트워크 복원력 11 published 2025-04-26
3W - 타임아웃, 재시도를 활용한 네트워크 복원력 12 published 2025-04-26
4W - 이스티오 메트릭 확인 13 published 2025-05-03
4W - 이스티오 메트릭 커스텀, 프로메테우스와 그라파나 14 published 2025-05-03
4W - 오픈텔레메트리 기반 트레이싱 예거 시각화, 키알리 시각화 15 published 2025-05-03
4W - 번외 - 트레이싱용 심플 메시 서버 개발 16 published 2025-05-03
5W - 이스티오 mTLS와 SPIFFE 17 published 2025-05-11
5W - 이스티오 JWT 인증 18 published 2025-05-11
5W - 이스티오 인가 정책 설정 19 published 2025-05-11
6W - 이스티오 설정 트러블슈팅 20 published 2025-05-18
6W - 이스티오 컨트롤 플레인 성능 최적화 21 published 2025-05-18
8W - 가상머신 통합하기 22 published 2025-06-01
8W - 엔보이와 iptables 뜯어먹기 23 published 2025-06-01
9W - 앰비언트 모드 구조, 원리 24 published 2025-06-07
9W - 앰비언트 헬름 설치, 각종 리소스 실습 25 published 2025-06-07
7W - 이스티오 메시 스케일링 26 published 2025-06-09
7W - 엔보이 필터를 통한 기능 확장 27 published 2025-06-09

관련 문서

지식 문서, EXPLAIN

이름7is-folder생성 일자
E-이스티오 가상머신 통합false2025-06-01 13:32
E-이스티오 DNS 프록시 동작false2025-06-01 12:33
E-앰비언트 ztunnel 트래픽 경로 분석false2025-06-07 20:36
E-iptables와 nftables의 차이false2024-12-30 21:38
iptablesfalse2024-12-30 21:37
kube-proxyfalse2025-02-12 12:42
E-이스티오의 데이터 플레인 트래픽 세팅 원리false2025-05-27 21:55

기타 문서

Z0-연관 knowledge, Z1-트러블슈팅 Z2-디자인,설계, Z3-임시, Z5-프로젝트,아카이브, Z8,9-미분류,미완
이름4코드타입생성 일자
2주차 - 네트워크Z5project2025-02-15 10:56
9W - 앰비언트 모드 구조, 원리Z8published2025-06-07 19:17
8W - 가상머신 통합하기Z8published2025-06-01 12:11
9W - 앰비언트 헬름 설치, 각종 리소스 실습Z8published2025-06-07 19:27

참고


  1. https://www.linux.co.kr/bbs/board.php?bo_table=lecture&wr_id=3763 ↩︎

  2. https://ssup2.github.io/blog-software/docs/theory-analysis/linux-conntrack/ ↩︎

  3. https://jimmysong.io/en/blog/sidecar-injection-iptables-and-traffic-routing/#understand-outbound-handler ↩︎

  4. https://istio.io/latest/docs/ambient/architecture/hbone/ ↩︎

  5. https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#envoy-v3-api-enum-config-cluster-v3-cluster-discoverytype ↩︎

  6. https://istio.io/latest/docs/ops/diagnostic-tools/proxy-cmd/ ↩︎

  7. https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/listener/v3/listener.proto#config-listener-v3-listener ↩︎